<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>XiaoZhi WebRTC</title>
    <link rel="icon" type="image/x-icon" href="static/favicon.ico">
    <script src="static/js/vue.min.js"></script>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
            margin: 0;
            padding: 0;
            background: #ededed;
            color: #333;
            height: 100vh;
            overflow: hidden;
        }

        #app {
            height: 100vh;
            position: relative;
            overflow: hidden;
        }

        .main-page {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100vh;
            opacity: 1;
            transform: translateY(0);
        }

        .chat-container {
            width: 100%;
            height: 100vh;
            background: #ededed;
            position: relative;
        }

        .control-button {
            background: #07C160;
            color: white;
            border: none;
            padding: 0.5rem 1.5rem;
            border-radius: 4px;
            font-size: 0.9rem;
            cursor: pointer;
            transition: all 0.3s ease;
        }

        .control-button:hover {
            background: #06ae56;
        }

        .control-button.stop {
            background: #f44336;
        }

        .control-button.stop:hover {
            background: #da190b;
        }

        #media {
            width: 100%;
            height: 100vh;
            background: #fff;
            position: relative;
        }

        .video-container {
            position: relative;
            width: 100%;
            height: 100%;
        }

        .main-video {
            position: absolute;
            top: 10%;
            left: 20%;
            right: 20%;
            bottom: 18%;
            background: #000;
            border-radius: 12px;
            overflow: hidden;
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
        }

        .main-video video {
            width: 100%;
            height: 100%;
            background: #000;
            object-fit: cover;
        }

        .small-video {
            position: absolute;
            bottom: calc(18% + 20px);
            left: calc(20% + 20px);
            width: 200px;
            height: 150px;
            background: #000;
            border-radius: 8px;
            border: 3px solid #fff;
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
            cursor: pointer;
            transition: all 0.3s ease;
            z-index: 100;
        }

        .small-video:hover {
            transform: scale(1.05);
            border-color: #07C160;
        }

        .small-video video {
            width: 100%;
            height: 100%;
            background: #000;
            border-radius: 5px;
            object-fit: cover;
        }

        .video-label {
            position: absolute;
            top: 8px;
            left: 8px;
            background: rgba(0, 0, 0, 0.7);
            color: white;
            padding: 4px 8px;
            border-radius: 4px;
            font-size: 12px;
            font-weight: 500;
            z-index: 10;
        }

        .video-controls {
            position: absolute;
            bottom: 6%;
            left: 50%;
            transform: translateX(-50%);
            display: flex;
            align-items: center;
            justify-content: space-between;
            width: 90vw;
            max-width: 500px;
            height: 60px;
            z-index: 200;
            padding: 0 12px;
        }

        .video-control-btn {
            background: #f5f5f5;
            color: #666;
            border: none;
            width: 50px;
            height: 50px;
            border-radius: 50%;
            font-size: 20px;
            cursor: pointer;
            transition: all 0.3s ease;
            display: flex;
            align-items: center;
            justify-content: center;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
        }

        .video-control-btn:hover {
            transform: scale(1.05);
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
        }

        .video-control-btn.close {
            background: rgb(255, 242, 242);
            color: #ff4757;
            font-size: 24px;
            box-shadow: 0 2px 8px rgba(255, 71, 87, 0.1);
            position: relative;
        }

        .video-control-btn.close:hover {
            background: rgb(255, 230, 230);
            color: #ff3742;
        }

        .video-control-btn.video-btn {
            position: relative;
        }

        .video-control-btn.video-btn.active,
        .video-control-btn.video-btn.inactive {
            background: rgb(247, 248, 252);
            color: #666;
        }

        .video-control-btn .icon {
            font-size: inherit;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .video-control-btn .icon svg {
            width: 20px;
            height: 20px;
            display: block;
        }

        .status-indicator {
            color: #333;
            font-size: 18px;
            font-weight: 500;
            user-select: none;
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .status-indicator.speaking { color: #06ae56; }
        .status-indicator.listening { color: #2980b9; }

        @media (max-width: 480px) {
            .video-control-btn {
                width: 44px;
                height: 44px;
                font-size: 18px;
            }
            .video-controls {
                height: 56px;
                padding: 0 8px;
            }
            .status-indicator {
                font-size: 16px;
                gap: 6px;
            }
        }

        .message-bubbles-container {
            position: fixed;
            bottom: 20px;
            right: 20px;
            width: 300px;
            max-height: 90%;
            display: flex;
            flex-direction: column;
            gap: 8px;
            z-index: 1000;
            pointer-events: none;
            overflow-y: auto;
            overflow-x: hidden;
            scrollbar-width: none;
            -ms-overflow-style: none;
        }

        .message-bubbles-container::-webkit-scrollbar { display: none; }

        .message-bubble {
            background: rgba(255, 255, 255, 0.95);
            padding: 12px 16px;
            border-radius: 18px;
            box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
            font-size: 14px;
            line-height: 1.4;
            word-break: break-word;
            max-width: 100%;
            animation: bubbleSlideIn 0.4s ease-out;
            pointer-events: auto;
            backdrop-filter: blur(10px);
            border: 1px solid rgba(255, 255, 255, 0.2);
            flex-shrink: 0;
        }

        .message-bubble.user { background: rgba(149, 236, 105, 0.95); color: #333; align-self: flex-end; margin-left: 40px; }
        .message-bubble.assistant { background: rgba(255, 255, 255, 0.95); color: #333; align-self: flex-start; margin-right: 40px; }

        @keyframes bubbleSlideIn { from { opacity: 0; transform: translateY(20px) scale(0.8);} to { opacity: 1; transform: translateY(0) scale(1);} }

        /* Loading overlay for /api/offer */
        .loading-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100vw;
            height: 100vh;
            background: rgba(0, 0, 0, 0.4);
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 9999;
        }

        .loading-box {
            background: rgba(255, 255, 255, 0.95);
            border-radius: 12px;
            padding: 16px 20px;
            display: flex;
            align-items: center;
            gap: 12px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.2);
            border: 1px solid rgba(0,0,0,0.06);
        }

        .spinner {
            width: 22px;
            height: 22px;
            border: 3px solid #e0e0e0;
            border-top-color: #07C160;
            border-radius: 50%;
            animation: spin 1s linear infinite;
        }

        @keyframes spin { to { transform: rotate(360deg); } }
    </style>
</head>

<body>
    <div id="app">
        <div class="main-page">
            <div class="chat-container">
                <div class="loading-overlay" v-if="isLoadingOffer">
                    <div class="loading-box">
                        <div class="spinner"></div>
                        <div>正在建立连接…</div>
                    </div>
                </div>
                <div id="media">
                    <div class="video-container">
                        <div class="main-video">
                            <div class="video-label">{{ isLocalVideoMain ? '本地视频' : '远程视频' }}</div>
                            <video ref="mainVideo" autoplay :muted="isLocalVideoMain" playsinline></video>
                        </div>

                        <div class="small-video" @click="switchVideoPosition">
                            <div class="video-label">{{ isLocalVideoMain ? '远程视频' : '本地视频' }}</div>
                            <video ref="smallVideo" autoplay :muted="!isLocalVideoMain" playsinline></video>
                        </div>

                        <div class="video-controls">
                            <button class="video-control-btn close" @click="endAndBack" title="结束通话">
                                <span class="icon">✕</span>
                            </button>

                            <div class="status-indicator" :class="{ speaking: speakingState === 'speaking', listening: speakingState === 'listening' }">
                                {{ speakingState === 'listening' ? '我在听' : '我在说话' }}
                            </div>

                            <button class="video-control-btn video-btn" :class="{ 'active': showLocalVideo, 'inactive': !showLocalVideo }" @click="toggleLocalVideo" :title="showLocalVideo ? '关闭本地视频' : '开启本地视频'">
                                <span class="icon" v-if="showLocalVideo">
                                    <svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M907.712 642.592l-2.624-302.592-204.256 145.056 206.88 157.536z m-39.68-354.784a64 64 0 0 1 101.056 51.648l2.624 302.592a64 64 0 0 1-102.752 51.456l-206.912-157.536a64 64 0 0 1 1.728-103.104l204.256-145.056z" fill="currentColor"></path><path d="M144 256a32 32 0 0 0-32 32v417.376a32 32 0 0 0 32 32h456.32a32 32 0 0 0 32-32V288a32 32 0 0 0-32-32H144z m0-64h456.32a96 96 0 0 1 96 96v417.376a96 96 0 0 1-96 96H144a96 96 0 0 1-96-96V288a96 96 0 0 1 96-96z" fill="currentColor"></path></svg>
                                </span>
                                <span class="icon" v-else>
                                    <svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg"><path d="M106.912 152.096A32 32 0 1 1 149.12 103.904l768 672a32 32 0 0 1-42.176 48.192l-768-672z" fill="currentColor"></path><path d="M732.672 462.4l-37.056-52.16 172.416-122.432a64 64 0 0 1 101.056 51.648l2.624 302.592a63.904 63.904 0 0 1-20.16 47.2l-43.84-46.656-2.624-302.592-172.416 122.432z m-34.72-54.016l35.616 53.184c-10.752 7.2-24.32 12.16-37.216 12.16a64 64 0 0 1-64-64V288a32 32 0 0 0-32-32h-205.952V192h205.952a96 96 0 0 1 96 96v121.28c0.416-0.224 1.088-0.544 1.6-0.896zM632.32 608h64v97.376a96 96 0 0 1-96 96H144a96 96 0 0 1-96-96V288a96 96 0 0 1 96-96h96v64h-96a32 32 0 0 0-32 32v417.376a32 32 0 0 0 32 32h456.32a32 32 0 0 0 32-32V608z" fill="currentColor"></path></svg>
                                </span>
                            </button>
                        </div>
                    </div>
                </div>
            </div>

            <div class="message-bubbles-container">
                <div v-for="(message, index) in displayMessages" :key="index" :class="['message-bubble', message.role === 'user' ? 'user' : 'assistant']">
                    {{ message.content }}
                </div>
            </div>
        </div>
    </div>

    <script>
        function getQueryParam(name) {
            const params = new URLSearchParams(window.location.search);
            return params.get(name) || '';
        }

        window.chatApp = new Vue({
            el: '#app',
            data: {
                start_time: 0,
                pc: null,
                localStream: null,
                remoteStream: null,
                isStarted: false,
                isConnected: false,
                status: null,
                dataChannel: null,
                messages: [],
                open_xiaozhi_url: true,
                maxMessages: 20,
                macAddress: '',
                isLocalVideoMain: false,
                showLocalVideo: false,
                speakingState: 'listening',
                currentAudio: null,
                isLoadingOffer: false,
            },
            mounted() {
                this.macAddress = getQueryParam('mac');
                this.initializeMedia().then(() => {
                    this.start();
                });
            },
            computed: {
                displayMessages() {
                    return this.messages.slice(-this.maxMessages);
                },
            },
            methods: {
                async initializeMedia() {
                    try {
                        this.localStream = await navigator.mediaDevices.getUserMedia({
                            video: { width: 640, height: 480 },
                            audio: {
                                    echoCancellation: true,
                                    echoCancellationType: "browser", // 使用浏览器内置回声消除，通常更稳定
                                    noiseSuppression: true,
                                    autoGainControl: true,
                                    latency: 0.02, // 降低延迟到20ms
                                    sampleRate: 48000,
                                    sampleSize: 16,
                                    channelCount: 1
                                }
                        });
                    } catch (error) {
                        console.error('Error accessing media devices:', error);
                    }
                },
                scrollToBottom() {
                    this.$nextTick(() => {
                        const container = document.querySelector('.message-bubbles-container');
                        if (container) {
                            container.scrollTop = container.scrollHeight;
                        }
                    });
                },
                async negotiate() {
                    try {
                        if (!this.pc) return;

                        // 创建 offer
                        const offer = await this.pc.createOffer();
                        if (!this.pc) return;

                        await this.pc.setLocalDescription(offer);

                        // 优化：不等待ICE收集完成，直接发送offer
                        // 这可以显著减少连接建立时间

                        if (!this.pc) return;
                        this.isLoadingOffer = true;
                        this.start_time = performance.now();

                        const response = await fetch('/api/offer', {
                            method: 'POST',
                            headers: { 'Content-Type': 'application/json' },
                            body: JSON.stringify({
                                sdp: this.pc.localDescription.sdp,
                                type: this.pc.localDescription.type,
                                macAddress: this.macAddress,
                            })
                        });

                        const answer = await response.json();
                        if (!this.pc) return;

                        await this.pc.setRemoteDescription(answer);

                    } catch (error) {
                        console.error('Error during negotiation:', error);
                        alert('Error during negotiation: ' + error.message);
                    } finally {
                        // this.isLoadingOffer = false;
                    }
                },
                setupDataChannel() {
                    this.dataChannel = this.pc.createDataChannel('chat');
                    this.dataChannel.onopen = () => { console.log('Data channel is open'); };
                    this.dataChannel.onmessage = (event) => {
                        this.messages.push(event.data);
                        this.$nextTick(() => { this.scrollToBottom(); });
                    };
                    this.dataChannel.onclose = () => { console.log('Data channel closed'); };
                    this.pc.ondatachannel = (event) => {
                        const channel = event.channel;
                        channel.onmessage = (e) => {
                            const data = JSON.parse(e.data);
                            if (data["type"] == "tts" && data["state"] == "sentence_start") {
                                this.speakingState = "speaking";
                            } else {
                                this.speakingState = "listening";
                            }
                            if (data["type"] === "music") {
                                this.playMusicUrl(data["url"]);
                            }
                            if (data["type"] === "websocket" && data["state"] === "close") {
                                this.messages.push({"role": "assistant", "content": "连接已断开（WebSocket 已关闭）"});
                                this.dataChannel.close();
                                this.pc.close();
                                this.endAndBack();
                                return;
                            }
                            if (data["type"] === "stt") {
                                this.messages.push({ "role": "user", "content": data["text"] });
                            } else if (data["type"] === "tts" && data["state"] === "sentence_start") {
                                this.messages.push({ "role": "assistant", "content": data["text"] });
                                if (data["text"].includes("请登录到控制面板添加设备，输入验证码：") && this.open_xiaozhi_url) {
                                    window.open("https://xiaozhi.me/console", "_blank");
                                    this.open_xiaozhi_url = false;
                                }
                            } else if (data["type"] === "tool") {
                                if (data["text"] === "set_volume") {
                                    if (this.isLocalVideoMain && this.$refs.smallVideo) {
                                        this.$refs.smallVideo.volume = Math.max(0, Math.min(1, data["value"] / 100));
                                    } else if (!this.isLocalVideoMain && this.$refs.mainVideo) {
                                        this.$refs.mainVideo.volume = Math.max(0, Math.min(1, data["value"] / 100));
                                    }
                                } else if (data["text"] === "open_tab") {
                                    window.open(data["value"], "_blank");
                                }
                            }
                            this.$nextTick(() => { this.scrollToBottom(); });
                        };
                    };
                },
                async start() {
                    if (this.pc) {
                        this.stop();
                    }

                    // 获取服务端 ICE 配置
                    let iceConfig = { iceServers: [], iceCandidatePoolSize: 0 };
                    try {
                        const resp = await fetch('/api/ice');
                        const data = await resp.json();
                        iceConfig = data;
                        console.log('从服务器获取ICE配置:', iceConfig);
                        if (!iceConfig.iceServers || iceConfig.iceServers.length === 0) {
                            console.warn('未配置 ICE 服务器，仅支持局域网直连');
                        } else {
                            console.log(`已配置 ${iceConfig.iceServers.length} 个ICE服务器，支持跨NAT连接`);
                        }
                    } catch (e) {
                        console.warn('获取 /api/ice 失败，使用空配置:', e);
                        iceConfig = { iceServers: [], iceCandidatePoolSize: 0 };
                    }

                    this.pc = new RTCPeerConnection(iceConfig);
                    this.status = "connecting...";
                    this.setupDataChannel();
                    this.localStream.getAudioTracks().forEach(track => { this.pc.addTrack(track, this.localStream); });
                    this.localStream.getVideoTracks().forEach(track => { this.pc.addTrack(track, this.localStream); });
                    this.pc.addEventListener('track', (evt) => {
                        this.remoteStream = evt.streams[0];
                        this.$nextTick(() => { this.assignVideoStreams(); });
                    });
                    this.pc.addEventListener('connectionstatechange', () => {
                        this.status = this.pc.connectionState;
                        if (this.pc.connectionState === 'connected') {
                            console.log('Connection state changed to connected in', performance.now() - this.start_time, 'ms');
                            this.isConnected = true;
                            this.isLoadingOffer = false;
                            this.$nextTick(() => { this.assignVideoStreams(); });
                        }
                    });
                    this.isStarted = true;
                    this.negotiate();
                },
                stop() {
                    this.isStarted = false;
                    this.isConnected = false;
                    this.status = null;
                    if (this.$refs.mainVideo) { this.$refs.mainVideo.srcObject = null; }
                    if (this.$refs.smallVideo) { this.$refs.smallVideo.srcObject = null; }
                    this.remoteStream = null;
                    if (this.dataChannel) { this.dataChannel.close(); this.dataChannel = null; }
                    if (this.pc) { this.pc.close(); this.pc = null; }
                },
                endAndBack() {
                    this.stop();
                    // window.location.href = '/';
                },
                switchVideoPosition() {
                    this.isLocalVideoMain = !this.isLocalVideoMain;
                    this.$nextTick(() => { this.assignVideoStreams(); });
                },
                toggleLocalVideo() {
                    this.showLocalVideo = !this.showLocalVideo;
                    this.$nextTick(() => { this.assignVideoStreams(); });
                },
                assignVideoStreams() {
                    const mainVideo = this.$refs.mainVideo;
                    const smallVideo = this.$refs.smallVideo;
                    if (!mainVideo || !smallVideo) { return; }
                    if (!this.showLocalVideo) {
                        if (this.remoteStream) {
                            mainVideo.srcObject = this.remoteStream;
                            mainVideo.style.transform = 'none';
                        }
                        smallVideo.srcObject = null;
                        smallVideo.style.display = 'none';
                        return;
                    }
                    smallVideo.style.display = 'block';
                    if (this.isLocalVideoMain) {
                        if (this.localStream) {
                            mainVideo.srcObject = this.localStream;
                            mainVideo.style.transform = 'scaleX(-1)';
                        }
                        if (this.remoteStream) {
                            smallVideo.srcObject = this.remoteStream;
                            smallVideo.style.transform = 'none';
                        }
                    } else {
                        if (this.remoteStream) {
                            mainVideo.srcObject = this.remoteStream;
                            mainVideo.style.transform = 'none';
                        }
                        if (this.localStream) {
                            smallVideo.srcObject = this.localStream;
                            smallVideo.style.transform = 'scaleX(-1)';
                        }
                    }
                },
                playMusicUrl(url) {
                    try {
                        if (this.currentAudio) {
                            this.currentAudio.pause();
                            this.currentAudio.currentTime = 0;
                            this.currentAudio = null;
                        }
                        const audio = new Audio(url);
                        audio.crossOrigin = 'anonymous';
                        audio.volume = 0.8;
                        this.currentAudio = audio;
                        audio.play().catch(() => { this.currentAudio = null; });
                        audio.addEventListener('ended', () => { if (this.currentAudio === audio) { this.currentAudio = null; } });
                        audio.addEventListener('error', () => { if (this.currentAudio === audio) { this.currentAudio = null; } });
                    } catch (error) {
                        console.error('Error creating audio:', error);
                    }
                }
            }
        });
    </script>
</body>

</html>
